package com.xr.system.common.config.multiRequestBodyConfig;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.xr.system.common.annotation.MultiRequestBody;
import com.xr.system.common.utils.JsonUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.ClassUtils;
import org.springframework.util.StreamUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver;

import javax.servlet.ServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.temporal.Temporal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * MultiRequestBody解析器 解决的问题： 1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收；
 * 2、多个对象需要封装到一个对象里才可以用@RequestBody接收。 主要优势： 1、支持通过注解的value指定JSON的key来解析对象。
 * 2、支持通过注解无value，直接根据参数名来解析对象 3、支持基本类型的注入 4、支持GET和其他请求方式注入
 * 5、支持通过注解无value且参数名不匹配JSON串key时，根据属性解析对象。
 * 6、支持多余属性(不解析、不报错)、支持参数“共用”（不指定value时，参数名不为JSON串的key）
 * 7、支持当value和属性名找不到匹配的key时，对象是否匹配所有属性。
 */
public class MultiRequestBodyArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

	private static final String JSON_BODY_ATTRIBUTE = "JSON_REQUEST_BODY";

	public MultiRequestBodyArgumentResolver(List<HttpMessageConverter<?>> converters) {
		super(converters);
	}

	public MultiRequestBodyArgumentResolver(List<HttpMessageConverter<?>> converters,
			List<Object> requestResponseBodyAdvice) {
		super(converters, requestResponseBodyAdvice);
	}

	/**
	 * 设置支持的方法参数类型
	 * @param parameter 方法参数
	 * @return 支持的类型
	 */
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		// 支持带@MultiRequestBody注解的参数
		return parameter.hasParameterAnnotation(MultiRequestBody.class);
	}

	/**
	 * 参数解析，利用jackson 注意：非基本类型返回null会报空指针异常，要通过反射或者JSON工具类创建一个空对象
	 */
	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = parameter.getParameterName();
		WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
		if (arg != null) {
			validateIfApplicable(binder, parameter);
			if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
				throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
			}
		}
		mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
		return adaptArgumentIfNecessary(arg, parameter);
	}

	@Override
	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
		ServletServerHttpRequest httpRequest = (ServletServerHttpRequest) inputMessage;
		String body = getRequestBody(httpRequest.getServletRequest());
		Object object = getParameterValue(JsonUtil.string2Bean(body, Map.class, String.class, Object.class), parameter,
				targetType);
		String string = ObjectUtil.isEmpty(object) ? null : JsonUtil.objectToJson(object);
		MultiHttpServletRequestWrapper wrapper = new MultiHttpServletRequestWrapper(httpRequest.getServletRequest(),
				string);
		Object arg = super.readWithMessageConverters(new ServletServerHttpRequest(wrapper), parameter, targetType);
		if (arg == null) {
			if (checkRequired(parameter)) {
				throw new HttpMessageNotReadableException("Required request body is missing: "
						+ Objects.requireNonNull(parameter.getMethod()).toGenericString(), inputMessage);
			}
		}
		return arg;
	}

	protected boolean checkRequired(MethodParameter parameter) {
		return (Objects.requireNonNull(parameter.getParameterAnnotation(MultiRequestBody.class)).required()
				&& !parameter.isOptional());
	}

	protected Object getParameterValue(Map<String, Object> map, MethodParameter parameter, Type paramType) {
		// 根据@MultiRequestBody注解value作为json解析的key
		MultiRequestBody parameterAnnotation = parameter.getParameterAnnotation(MultiRequestBody.class);
		String defaultValue = parameterAnnotation.defaultValue();
		if (map != null && !map.isEmpty()) {
			// 注解的value是JSON的key
			// 注解为设置value则用参数名当做json的key
			String key = StrUtil.isNotEmpty(parameterAnnotation.value()) ? parameterAnnotation.value()
					: parameter.getParameterName();
			// 如果@MultiRequestBody注解没有设置value，则取参数名FrameworkServlet作为json解析的key
			Object value = map.get(key);
			// 如果设置了value但是解析不到，报错
			if (ObjectUtil.isEmpty(value) && ObjectUtil.isNotEmpty(defaultValue)) {
				value = defaultValue;
			}
			// 通过注解的value或者参数名解析，能拿到value进行解析
			if (value == null) {
				if (parameterAnnotation.parseAllFields()) {
					// 非基本类型，允许解析，将外层属性解析
					if (!ClassUtils.isPrimitiveOrWrapper(parameter.getParameterType())
							&& parameter.getParameterType() != Date.class
							&& parameter.getParameterType() != String.class
							&& !Date.class.isAssignableFrom(parameter.getParameterType())
							&& !Temporal.class.isAssignableFrom(parameter.getParameterType())) {
						value = JsonUtil.map2pojo(map, paramType);
					}
				}
			}
			return value;
		}
		else {
			return ObjectUtil.isEmpty(defaultValue) ? null : defaultValue;
		}
	}

	protected String getRequestBody(ServletRequest request) {
		// 有就直接获取
		String body = (String) request.getAttribute(JSON_BODY_ATTRIBUTE);
		// 没有就从请求中读取
		if (body == null) {
			try {
				body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
				request.setAttribute(JSON_BODY_ATTRIBUTE, body);
			}
			catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
		return body;
	}

}
